package org.ccay.core.util;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.CharacterData;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import org.ccay.core.util.exception.DomException;

/**
 * DOM文件解析工具
 * 
 * 
 * @since 2011-11-5
 */
public abstract class DomUtil {

	private final static String EX_CREATE_FAIL = "ccay.core.00060001";

	private final static String EX_PARSE_FAIL = "ccay.core.00060002";
	/**
	 * 重载 Document loadDocument(InputSource inputSource,
			ErrorHandler errorHandler) throws IOException
	 * 避免传递null错误处理
	 * @param inputSource
	 * @return
	 * @throws IOException
	 */
	public static Document loadDocument(InputSource inputSource ) throws IOException {
		return loadDocument( inputSource,null);
	}
	/**
	 * 读取DOM文件，并解析
	 * 
	 * @param inputSource
	 *            DOM文件来源
	 * @param errorHandler
	 *            错误处理器
	 * @return <code>org.w3c.dom.Document</code>
	 * @throws Exception
	 * @see org.xml.sax.InputSource
	 * @see org.xml.sax.ErrorHandler
	 */
	public static Document loadDocument(InputSource inputSource,
			ErrorHandler errorHandler) throws IOException {

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		DocumentBuilder builder = createDocumentBuilder(factory, errorHandler);

		try {
			return builder.parse(inputSource);
		} catch (SAXException saxe) {
			throw new DomException(EX_PARSE_FAIL, saxe);
		}
	}

	/**
	 * 获取下一层符合给定的节点名称的Element 列， 不同于DOM API中的getElementsByTagName方法
	 * 
	 * @param ele
	 *            DOM元素
	 * @param childEleNames
	 *            预获取的节点名称数组
	 * @return <code>org.w3c.dom.Element</code>类的List
	 * @see org.w3c.dom.Element
	 * @see org.w3c.dom.Element#getElementsByTagName
	 */
	public static List<Element> getChildElementsByTagName(Element ele,
			String[] childEleNames) {
		AssertUtil.assertObjectNotNull(ele, "Element must not be null");
		AssertUtil.assertObjectNotNull(childEleNames,
				"Element names collection must not be null");
		List<String> childEleNameList = Arrays.asList(childEleNames);
		NodeList nl = ele.getChildNodes();
		List<Element> childEles = new ArrayList<Element>();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (node instanceof Element
					&& nodeNameMatch(node, childEleNameList)) {
				childEles.add((Element) node);
			}
		}
		return childEles;
	}

	/**
	 * 获取下一层符合给定的节点名称的Element 列， 不同于DOM API中的getElementsByTagName方法
	 * 
	 * @param ele
	 *            DOM元素
	 * @param childEleName
	 *            预获取的节点名称
	 * @return <code>org.w3c.dom.Element</code>类的List
	 * @see org.w3c.dom.Element
	 * @see org.w3c.dom.Element#getElementsByTagName
	 */
	public static List<Element> getChildElementsByTagName(Element ele,
			String childEleName) {
		return getChildElementsByTagName(ele, new String[] { childEleName });
	}

	/**
	 * 获取下一层第一个符合给定的节点名称的Element
	 * 
	 * @param ele
	 *            DOM元素
	 * @param childEleName
	 *            预获取的节点名称
	 * @return <code>org.w3c.dom.Element</code> 对象，没有时，回传<code>null</code>
	 */
	public static Element getChildElementByTagName(Element ele,
			String childEleName) {
		AssertUtil.assertObjectNotNull(ele, "Element must not be null");
		AssertUtil.assertObjectNotNull(childEleName,
				"Element name must not be null");
		NodeList nl = ele.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (node instanceof Element && nodeNameMatch(node, childEleName)) {
				return (Element) node;
			}
		}
		return null;
	}

	/**
	 * 获取下一层第一个符合给定的节点名称的Element文本
	 * 
	 * @param ele
	 *            DOM元素
	 * @param childEleName
	 *            预获取的节点名称
	 * @return 获取该Element的文本，没有时，回传<code>null</code>
	 */
	public static String getChildElementValueByTagName(Element ele,
			String childEleName) {
		Element child = getChildElementByTagName(ele, childEleName);
		return (child != null ? getTextValue(child) : null);
	}

	/**
	 * 获取给定的DOM Element下所有第一层的元素
	 * 
	 * @param ele
	 *            DOM元素
	 * @return <code>org.w3c.dom.Element</code> 对象的列
	 */
	public static List<Element> getChildElements(Element ele) {
		AssertUtil.assertObjectNotNull(ele, "Element must not be null");
		NodeList nl = ele.getChildNodes();
		List<Element> childEles = new ArrayList<Element>();
		for (int i = 0; i < nl.getLength(); i++) {
			Node node = nl.item(i);
			if (node instanceof Element) {
				childEles.add((Element) node);
			}
		}
		return childEles;
	}

	/**
	 * 获取给定的DOM Element第一层元素的所有文本，忽略注解
	 * 
	 * @see CharacterData
	 * @see EntityReference
	 * @see Comment
	 */
	public static String getTextValue(Element valueEle) {
		AssertUtil.assertObjectNotNull(valueEle, "Element must not be null");
		StringBuilder sb = new StringBuilder();
		NodeList nl = valueEle.getChildNodes();
		for (int i = 0; i < nl.getLength(); i++) {
			Node item = nl.item(i);
			if ((item instanceof CharacterData && !(item instanceof Comment))
					|| item instanceof EntityReference) {
				sb.append(item.getNodeValue());
			}
		}
		return sb.toString();
	}

	/**
	 * 比较元素名称是否与给定的名称相同，如果是，回传<code>true</code>，反之，则回传<code>false</code>
	 */
	public static boolean nodeNameEquals(Node node, String desiredName) {
		AssertUtil.assertObjectNotNull(node, "Node must not be null");
		AssertUtil.assertObjectNotNull(desiredName,
				"Desired name must not be null");
		return nodeNameMatch(node, desiredName);
	}
	
	/**
	 * 根据路径表达式查找
	 * 
	 * @param element
	 * @param path
	 * @return
	 */
	public static Element findElementByPath(Element element, QName[] path) {
		
		Element subElement = findSubElement( element,  path);
		//如果只有一个path 那么 直接返回节点 
		if(null == subElement || null == path || path.length<2){
			return subElement;
		}
		//如果有多个path的情况 那么取最后的path 截取 再返回节点
		QName[] subPath = new QName[path.length-1];
		System.arraycopy(path, 1, subPath, 0, path.length-1);
		return findElementByPath(subElement, subPath);

	}
	/**
	 * 获取当前的子节点
	 * @param element
	 * @param path
	 * @return
	 * Created：2012-9-6
	 * @author chaowangbang
	 */
	private static Element findSubElement(Element element, QName[] path){
		if(element==null || path==null) {
			return element;
		}
		QName qname = path[0];
		NodeList children = element.getElementsByTagNameNS(qname.getNamespaceURI(), qname.getLocalPart());
		//如果获取不到节点 返回null
		if(children==null || children.getLength()==0) {
			return null;
		}
		return (Element)children.item(0);
	}
	/**
	 * 根据路径表达式查找
	 * 
	 * @param element
	 * @param path
	 * @return
	 */
	public static Element findElementByPath(Element element, String path) {
		int index = path.indexOf('/');
		String elementName = null;
		if(index>0) {
			elementName = path.substring(0, index);
			String subPath = path.substring(index+1);
			
			NodeList children = element.getElementsByTagNameNS("*", elementName);
			if(children==null || children.getLength()==0) {
				return null;
			}
			
			return findElementByPath((Element)children.item(0), subPath);
		} else {
			elementName = path;
			NodeList children = element.getElementsByTagNameNS("*", elementName);
			return children==null?null:(Element)children.item(0);
		}
	}

	protected static DocumentBuilder createDocumentBuilder(
			DocumentBuilderFactory factory, ErrorHandler errorHandler) {

		DocumentBuilder docBuilder = null;
		try {
//			factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
			docBuilder = factory.newDocumentBuilder();
		} catch (ParserConfigurationException pce) {
			throw new DomException(EX_CREATE_FAIL, pce);
		}

		if (errorHandler != null) {
			docBuilder.setErrorHandler(errorHandler);
		}
		return docBuilder;
	}

	private static boolean nodeNameMatch(Node node, String desiredName) {
		return (desiredName.equals(node.getNodeName()) || desiredName
				.equals(node.getLocalName()));
	}

	private static boolean nodeNameMatch(Node node, Collection<?> desiredNames) {
		return (desiredNames.contains(node.getNodeName()) || desiredNames
				.contains(node.getLocalName()));
	}

}
